iOS链式编程实(仿Masonry框架)

前言: 相信做过iOS开发的都知道系统给开发者提供的代码设置约束有多难用,设置一个元素的位置往往要写十多行的代码,因为NSLayoutConstraint这个类是在太难用了。所以一个简单的代码布局框架就会很受开发者欢迎。所以也有了这次实践,通过链式编程实现简单的自动布局。下面也是模仿Masonry做一套自己的自动布局框架。

1.先看看我们最终实现同样的效果的代码对比

1.1使用系统代码设置约束

greenView.translatesAutoresizingMaskIntoConstraints = NO;
[superview addConstraints:@[

                            //greenView constraints
                            [NSLayoutConstraint constraintWithItem:greenView
                                                         attribute:NSLayoutAttributeTop
                                                         relatedBy:NSLayoutRelationEqual
                                                            toItem:superview
                                                         attribute:NSLayoutAttributeTop
                                                        multiplier:1.0
                                                          constant:20],

                            [NSLayoutConstraint constraintWithItem:greenView
                                                         attribute:NSLayoutAttributeLeft
                                                         relatedBy:NSLayoutRelationEqual
                                                            toItem:superview
                                                         attribute:NSLayoutAttributeLeft
                                                        multiplier:1.0
                                                          constant:20],

                            [NSLayoutConstraint constraintWithItem:greenView
                                                         attribute:NSLayoutAttributeBottom
                                                         relatedBy:NSLayoutRelationEqual
                                                            toItem:nil
                                                         attribute:NSLayoutAttributeNotAnAttribute
                                                        multiplier:1.0
                                                          constant:80],

                            [NSLayoutConstraint constraintWithItem:greenView
                                                         attribute:NSLayoutAttributeRight
                                                         relatedBy:NSLayoutRelationEqual
                                                            toItem:superview
                                                         attribute:NSLayoutAttributeNotAnAttribute
                                                        multiplier:1
                                                          constant:80],

                            ]];

1.2使用链式编程实现约束

1
2
3
4
5
6
[greenView makeLayout:^void(LMLayout *layout) {
layout.width.equalTo(@(80));
layout.height.equalTo(@80);
layout.left.equalTo(self.view.lm_left).offSet(20);
layout.top.equalTo(self.view.lm_top).offSet(20);
}];

2.以上的代码缩减了这么多主要是因为将复杂的方法调用用链式的方式实现出来了,那么什么是链式编程呢?

1
layout.left.equalTo(self.view.lm_left).offSet(20);
以这句代码为例,可以通过点语法一直不断的调用下一个方法,像一条链子一样,这样的方式就叫做链式编程。

那么在代码层面,如何可以实现一直不断的通过点语法调用下一个方法。
实质上就是通过点语法调用方法的时候,返回一个block。而这个block必须需要包涵一个很重要的特点,这个block有返回值,这个返回值的类型是一个调用下一个方法的对象,这样下次又可以通过这个对象执行下一个方法。代码示例为:

1
-(LMLayout *(^)(NSObject *object))equalTo;

上述代码返回了一个(LMLayout (^)(NSObject object)block,该block返回了一个LMLayout对象,以便后面可以继续调用下一个方法,然后接受输入一个object的参数,该参数可以用来设置内部的约束的值。

3.基于以上的了解,我们开始着手写一个仿masonry的框架。

以下使我们最终要实现的调用方式

1
2
3
4
5
6
[greenView makeLayout:^void(LMLayout *layout) {
layout.width.equalTo(@(80));
layout.height.equalTo(@80);
layout.left.equalTo(self.view.lm_left).offSet(20);
layout.top.equalTo(self.view.lm_top).offSet(20);
}];

3.1先从外层的方法看起,首先给UIView创建了一个分类,并且定义了一个makeLayout的方法,需要传进一个block的参数,该block的作用就是设置greenView的约束。所以最外层的方法就是

UIView+layout.h 文件

1
- (void)makeLayout:(void(^)(LMLayout *layout))layoutBlock;

UIView+layout.m文件

1
2
3
4
- (void)makeLayout:(void(^)(LMLayout *layout))layoutBlock{
LMLayout *layout = [[LMLayout alloc]initWithView:self];
layoutBlock(layout);
}

在以上的方法中,创建了一个LMLayout的对象,并且将当前的view作为参数去初始化LMLayout对象。接下来看看一些LMLayout的初始化方法的实现。

1
2
3
4
5
6
7
8
- (instancetype)initWithView:(UIView *)view{
NSAssert([view superview]!=nil, @"未能找到view的superView");
if (self = [super init]) {
_view = view;
view.translatesAutoresizingMaskIntoConstraints = NO;
}
return self;
}

在初始化方法内部会判断当前设置约束的view是否有superView,并且用属性保存当前传进来的view,关闭translatesAutoresizingMaskIntoConstraints。
再返回到上一个方法,初始化完成之后就执行该block,将初始化好的layout对象传过去。

3.2 接下来看block内部的调用

1
layout.width.equalTo(@(80));

一层一层分析,layout.width实质上是调用了-(LMLayout*)width; 看看该方法内部的实现。

1
2
3
4
-(LMLayout*)width{
_property = PropertyTypeWidth;
return self;
}

PropertyTypeWidth 是一个枚举值,表示当前设置约束的属性是width,然后返回当前layout对象。
既然返回了layout对象,那么同样可以通过点执行下一个方法

1
layout.equalTo(@(80))

同样看一下方法的定义和实现
.h

1
-(LMLayout *(^)(NSObject *object))equalTo;

.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-(LMLayout *(^)(NSObject *object))equalTo{
__weak typeof(self)weakSelf = self;
return ^(NSObject *object){
//判断外部调用equalTo()这个block的时候会传什么值
//如果是直接设置属性的值
if ([object isKindOfClass:[NSNumber class]]) {
CGFloat floatValue = [(NSNumber*)object floatValue];
NSLayoutAttribute attribute = NSLayoutAttributeHeight;
switch (weakSelf.property) {
case PropertyTypeWidth:
attribute = NSLayoutAttributeWidth;
break;
case PropertyTypeHeight:
attribute = NSLayoutAttributeHeight;
break;
default:
attribute = NSLayoutAttributeNotAnAttribute;
break;
}
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:weakSelf.view attribute:attribute relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:floatValue];
//检测之前是否已经设置了旧的同一属性的约束,如果是就删除,添加新的约束
[weakSelf checkIfExistOldLayoutWithNewLayout:constraint];
[weakSelf.view addConstraint:constraint];
}
//如果是设置一个参照view
else if([object isKindOfClass:[NSDictionary class]]){
NSDictionary *dict = (NSDictionary*)object;
UIView *view = (UIView *)dict[@"view"];
weakSelf.conferenceView = view;
weakSelf.conferProperty = [dict[@"property"] floatValue];
NSLayoutAttribute attribute = [LMLayout attributeWithProperty:weakSelf.property];
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:weakSelf.view attribute:attribute relatedBy:NSLayoutRelationEqual toItem:weakSelf.conferenceView attribute:weakSelf.conferProperty multiplier:1 constant:0];
//检测之前是否已经设置了旧的同一属性的约束,如果是就删除,添加新的约束
[weakSelf checkIfExistOldLayoutWithNewLayout:constraint];
weakSelf.currentConstraint = constraint;
[weakSelf.view.superview addConstraint:constraint];

}
return self;
};

上述方法首先返回一个返回值类型为LMLayout,参数类型型为NSObject对象的Block,为什么是NSObject对象类型,因为equalTo有可能会直接传入一个数值,也有可能会传入一个view的属性,如:

1
2
layout.height.equalTo(@80);
layout.left.equalTo(self.view.lm_left).offSet(20);

3.2.1 首先看传进来的是一个数值的情况

如果传进来是一个数值,那么会判断之前是否已经设置过该约束,如果已经设置过就删除该约束,重新添加新的约束。

3.2.2 如果是.equalTo(self.view.lm_left)这种形式,括号内的self.view.lm_left实质上是执行了UIView的分类的

1
-(NSDictionary<UIView *,NSNumber *>*)lm_left;

.m文件

1
2
3
4
5
6
7
-(NSDictionary<UIView *,NSNumber *>*)lm_left{
return [self dictWithProperty:PropertyTypeLeft];
}
- (NSDictionary<UIView *,NSNumber *>*)dictWithProperty:(PropertyType)type{
NSDictionary *dict = @{@"view":self,@"property":@([LMLayout attributeWithProperty:type])};
return dict;
}

方法内部会返回一个字典,字典的第一个参数表示需要参照的view,第二个参数表示的参照view的哪个属性。
当接收到返回的字典之后,因为之前已经获取了需要约束的view和需要约束的属性,现在也获取了参照的view和参照的属性,可以创建NSLayoutConstraint对象,并且添加约束到他们共同的superView上面。

3.3 下面来关注一下offset(20)

1
layout.left.equalTo(self.view.lm_left).offSet(20);

.h

1
-(void(^LayoutOffsetBlock)(CGFloat offset))offSet;

.m

1
2
3
4
5
6
7
8
9
10
11
12
-(void(^LayoutOffsetBlock)(CGFloat offset))offSet{
__weak typeof(self)weakSelf = self;
return ^(CGFloat margin){
NSLayoutAttribute attribute = [LMLayout attributeWithProperty:weakSelf.property];
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:weakSelf.view attribute:attribute relatedBy:NSLayoutRelationEqual toItem:weakSelf.conferenceView attribute:weakSelf.conferProperty multiplier:1 constant:margin];
if (weakSelf.currentConstraint.firstItem == constraint.firstItem && weakSelf.currentConstraint.firstAttribute == constraint.firstAttribute && weakSelf.currentConstraint.secondItem == constraint.secondItem && weakSelf.currentConstraint.secondAttribute == constraint.secondAttribute) {
weakSelf.currentConstraint.constant = margin;
//[weakSelf.view.superview removeConstraint:weakSelf.currentConstraint];
// [weakSelf.view.superview addConstraint:constraint];
}
};
}

该方法内部首先会根据枚举值拿到对应的NSLayoutAttribute,然后判断是否已经设置了该约束,如果是,那么直接修改该约束的值。

4 设置其他约束的方法都和上述类似。相关的源码已经上传到LMlayout,具体可以查看github.

-------评论系统采用disqus,如果看不到需要翻墙-------------